﻿#include "common.h"
#include "shared.h"
#include "context.h"

#ifdef _ANIP_WINDOWS
#include <Windows.h>
#endif

#include <sys/stat.h>
#include <iostream>
#include <fstream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <codecvt>
#include <locale>

#include <cstdarg>
#include <cwchar>

anip::InitStatus _status = anip::InitStatus::Uninitialized;

void anip::_error_callback(int error, const char* description)
{
	throw std::runtime_error(description);
}

void anip::debug(const char* format, ...)
{
	va_list l;
	va_start(l, format);
	vprintf(format, l);
	va_end(l);
}

void anip::debug(const wchar_t* format, ...)
{
	va_list l;
	va_start(l, format);
#ifdef _ANIP_WINDOWS
	size_t len = wcslen(format);
	int n = ::WideCharToMultiByte(CP_ACP, 0, format, len, nullptr, 0, nullptr, nullptr);
    string str(n, 0);
    ::WideCharToMultiByte(CP_ACP, 0, format, len, &str[0], n, nullptr, nullptr);
#else
	string str = ws2utf8s(format);
#endif
	vprintf(str.c_str(), l);
	va_end(l);
}

#ifdef USE_ERROR_CHECK

void anip::_errorCheck(const char* file, const char* line)
{
	static string errstring("???");
	GLenum err = glGetError();
	switch (err)
	{
	case GL_NO_ERROR:
		return;
	case GL_INVALID_ENUM:
		errstring = "INVALID_ENUM"; break;
	case GL_INVALID_VALUE:
		errstring = "INVALID_VALUE"; break;
	case GL_INVALID_OPERATION:
		errstring = "INVALID_OPERATION"; break;
	case GL_INVALID_FRAMEBUFFER_OPERATION:
		errstring = "INVALID_FRAMEBUFFER_OPERATION"; break;
	case GL_OUT_OF_MEMORY:
		errstring = "OUT_OF_MEMORY"; break;
	}
	errstring.append(" in ");
	errstring.append(file);
	errstring.append(", line ");
	errstring.append(line);

	throw std::runtime_error(errstring.c_str());
}

#endif

void anip::init()
{
	assert(_status == InitStatus::Uninitialized);

	glfwSetErrorCallback(_error_callback);
	if (!glfwInit())
		throw std::runtime_error("Cannot init GLFW");
	_status = InitStatus::Initialized;
}

void anip::finalize()
{
	assert(_status == InitStatus::Initialized);

	Shared::finalize();
	Context::finalize();
	glfwTerminate();
	_status = InitStatus::Finalized;
}

anip::InitStatus anip::initStatus()
{
	return _status;
}

bool anip::fs::fileExists(const string& name)
{
	static struct stat buffer;
	return (stat(name.c_str(), &buffer) == 0);
}

anip::string anip::fs::filename(const string & path, bool ext)
{
	auto i = path.rfind('/', path.length());
#ifdef _ANIP_WINDOWS
	auto j = path.rfind('\\', path.length());
	if (j != string::npos && j < i) i = j;
#endif
	if (i != string::npos) 
	{
		string n = path.substr(i+1, path.length() - i);
		if (!ext)
		{
			size_t k = n.find_last_of(".");
			if (k != string::npos) n = n.substr(0, k);
		}
		return n;
	}
	return path;
}

// extensions are with the dot (e.g. ".txt")
anip::string anip::fs::extension(const string& path)
{
	size_t k = path.find_last_of(".");
	if (k != string::npos) return path.substr(k);
	return "";
}

anip::string anip::fs::removeExtension(const string& path)
{
	size_t k = path.find_last_of(".");
	if (k != string::npos) return path.substr(0, k);
	return path;
}

anip::wstring anip::fs::path2ws(const string& path)
{
#ifdef _ANIP_WINDOWS
	int n = ::MultiByteToWideChar(CP_ACP, 0, path.c_str(), path.size(), nullptr, 0);
    wstring wstr(n, 0);
    ::MultiByteToWideChar(CP_ACP, 0, path.c_str(), path.size(), &wstr[0], n);
    return wstr;
#else
	return utf8s2ws(path);
#endif
}

anip::string anip::fs::ws2path(const wstring& path)
{
#ifdef _ANIP_WINDOWS
	int n = ::WideCharToMultiByte(CP_ACP, 0, path.c_str(), path.size(), nullptr, 0, nullptr, nullptr);
    string str(n, 0);
    ::WideCharToMultiByte(CP_ACP, 0, path.c_str(), path.size(), &str[0], n, nullptr, nullptr);
	return str;
#else
	return ws2utf8s(path);
#endif
}

double anip::time()
{
	return glfwGetTime();
}

anip::wstring anip::utf8s2ws(const string& str)
{
    static std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
    static int orderCheck = 0;
    std::wstring ws = cv.from_bytes(str);
    
	// I developed this workaround due to what seems like a bug in MinGW's
	// standard library that causes `cv.from_bytes` to return wide strings
	// with incorrect word order
    if (orderCheck == 0)
    {
        bool needSwap = cv.from_bytes(cv.to_bytes(L"\u1234"))[0] != L'\u1234';
        orderCheck = needSwap ? 1 : 2;
    }

    if (orderCheck == 1)
        for (size_t i = 0; i < ws.size(); i++)
            ws[i] = ((wchar_t)ws[i] << 8) 
					| ((wchar_t)ws[i] >> 8);
    
    return ws;
}

anip::string anip::ws2utf8s(const wstring& str)
{
	static std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
	return cv.to_bytes(str);
}

anip::wstring anip::toString(int x)
{
	// seems we have to use char[] instead of wchar_t[] because
	// there isn't a standardized wsnprintf (MSVC has wsprintf_s,
	// but only for Windows).
	static char buf[100];
	snprintf(buf, sizeof(buf), "%d", x);
	return utf8s2ws(buf);
}

anip::wstring anip::toString(float x)
{
	// see above
	static char buf[100];
	snprintf(buf, sizeof(buf), "%g", x);
	return utf8s2ws(buf);
}

anip::wstring anip::toString(double x)
{
	// see above
	static char buf[100];
	snprintf(buf, sizeof(buf), "%g", x);
	return utf8s2ws(buf);
}

bool anip::parseInt(wstring s, int& out, int radix)
{
	wchar_t* end = nullptr;
	out = (int)wcstol(s.c_str(), &end, radix);
	return end == s.c_str() + s.size();
}

bool anip::parseFloat(wstring s, float& out)
{
	wchar_t* end = nullptr;
	out = wcstof(s.c_str(), &end);
	return end == s.c_str() + s.size();
}

bool anip::parseDouble(wstring s, double& out)
{
	wchar_t* end = nullptr;
	out = wcstod(s.c_str(), &end);
	return end == s.c_str() + s.size();
}

std::vector<anip::wstring> anip::split(const wstring& str, const wstring& delim)
{
	std::vector<wstring> result;
	wstring partial = str;
	size_t pos = 0, off = 0;
	while ((pos = partial.find(delim, off)) != wstring::npos)
	{
		result.push_back(partial.substr(off, pos - off));
		off = pos + delim.length();
	}
	result.push_back(partial.substr(off));
	return result;
}

std::vector<anip::string> anip::split(const string& str, const string& delim)
{
	std::vector<anip::string> result;
	string partial = str;
	size_t pos = 0, off = 0;
	while ((pos = partial.find(delim, off)) != string::npos)
	{
		result.push_back(partial.substr(off, pos - off));
		off = pos + delim.length();
	}
	result.push_back(partial.substr(off));
	return result;
}

anip::string anip::readFile(const string& path)
{
	std::ifstream ifs(path, std::ios::in);
	if (!ifs.good())
		throw std::runtime_error("file system error");

	ifs.seekg(0, ifs.end);
	auto length = (unsigned int)ifs.tellg();
	ifs.seekg(0, ifs.beg);

	char* buffer = new char[length];
	ifs.read(buffer, length);
	ifs.close();

	// sometimes length != ifs.gcount()
	// and the buffer returned by ifs.read() isn't
	// zero-terminated...
	buffer[ifs.gcount()] = '\0';
	
	string content(buffer);
	delete[] buffer;
	return content;
}

anip::wstring anip::readUTF8File(const string& path)
{
	return utf8s2ws(readFile(path));
}

template<>
void anip::binaryWrite(const string& val, std::ostream& o)
{
	binaryWrite(val.length(), o);
	o.write(val.c_str(), val.length());
}

template<>
void anip::binaryWrite(const wstring& val, std::ostream& o)
{
	binaryWrite(ws2utf8s(val), o);
}

template<>
void anip::binaryRead(string& val, std::istream& i)
{
	size_t size;
	binaryRead(size, i);
	char* chs = new char[size];
	i.read(chs, size);
	val.assign(chs, size);
}

template<>
void anip::binaryRead(wstring& val, std::istream& i)
{
	string s;
	binaryRead(s, i);
	val = utf8s2ws(s);
}